﻿using System;
using System.Runtime.InteropServices;
using System.ComponentModel;

namespace SystemX.Infrastructure
{
	/// <summary>
	/// An <see cref="IIdentifierGenerator" /> that generates <see cref="System.Guid"/> values 
	/// using a strategy suggested Jimmy Nilsson's 
	/// <a href="http://www.informit.com/articles/article.asp?p=25862">article</a>
	/// on <a href="http://www.informit.com">informit.com</a>. 
	/// </summary>
	/// <remarks>
	/// <p>
	///	This id generation strategy is specified in the mapping file as 
	///	<code>&lt;generator class="guid.comb" /&gt;</code>
	/// </p>
	/// <p>
	/// The <c>comb</c> algorithm is designed to make the use of GUIDs as Primary Keys, Foreign Keys, 
	/// and Indexes nearly as efficient as ints.
	/// </p>
	/// <p>
	/// This code was contributed by Donald Mull.
	/// </p>
	/// </remarks>
	public static class GuidExt
	{
		public enum GuidStrategy { 
			/// <summary>
			/// using the comb algorithm
			/// </summary>
			NormalComb,
			/// <summary>
			/// using a strategy suggested Martin Bells's <a href="http://sqlblogcasts.com/blogs/martinbell/archive/2009/05/25/GUID-Fragmentation-in-SQL-Server.aspx">article</a>. 
			/// </summary>
			OrderedSequential
		}
		/// <summary>
		/// Generate a new <see cref="Guid"/> using the comb algorithm.
		/// </summary>
		/// <param name="session">The <see cref="ISessionImplementor"/> this id is being generated in.</param>
		/// <param name="obj">The entity for which the id is being generated.</param>
		/// <returns>The new identifier as a <see cref="Guid"/>.</returns>
		public static Guid NewGuid()
		{
			return GenerateComb();
		}
		public static Guid NewGuid(GuidStrategy strategy)
		{
			if (strategy == GuidStrategy.NormalComb)
			{
				return GenerateComb();
			}
			return GenerateOrderedSequentialGuid();
		}
		#region comb algorithm
		/// <summary>
		/// Generate a new <see cref="Guid"/> using the comb algorithm.
		/// </summary>
		private static Guid GenerateComb()
		{
			byte[] guidArray = Guid.NewGuid().ToByteArray();

			DateTime baseDate = new DateTime(1900, 1, 1);
			DateTime now = DateTime.Now;

			// Get the days and milliseconds which will be used to build the byte string 
			TimeSpan days = new TimeSpan(now.Ticks - baseDate.Ticks);
			TimeSpan msecs = now.TimeOfDay;

			// Convert to a byte array 
			// Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333 
			byte[] daysArray = BitConverter.GetBytes(days.Days);
			byte[] msecsArray = BitConverter.GetBytes((long)(msecs.TotalMilliseconds / 3.333333));

			// Reverse the bytes to match SQL Servers ordering 
			Array.Reverse(daysArray);
			Array.Reverse(msecsArray);

			// Copy the bytes into the guid 
			Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2);
			Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4);

			return new Guid(guidArray);
		}
		#endregion

		#region ordered sequential algorithm

		private const int RPC_S_OK = 0;

        // Used to sort the bytes of the Guid according to Alberto Ferrari article 
		//(http://sqlblog.com/blogs/alberto_ferrari/archive/2007/08/31/how-are-guids-sorted-by-sql-server.aspx)
        private static readonly int[] _sqlOrderMap = new[] {3, 2, 1,
0, 5, 4, 7, 6, 9, 8, 15, 14, 13, 12, 11, 10}; 
		[DllImport("rpcrt4.dll", SetLastError = true)]
		private static extern int UuidCreateSequential(out Guid guid);
		/// <summary>
		/// using a strategy suggested Martin Bells's <a href="http://sqlblogcasts.com/blogs/martinbell/archive/2009/05/25/GUID-Fragmentation-in-SQL-Server.aspx">article</a>. 
		/// </summary>
		/// <remarks>
		/// Two things to take in account:
		/// -Ids generated through this algorithm are "guessable" which might
		/// cause some security risk (no higher than int, however)
		/// -The computer generating the identifier requires a Ethernet address.
		/// Otherwise the code throws. 
		/// </remarks>
		/// <returns></returns>
		private static Guid GenerateOrderedSequentialGuid()
		{
			//sequential
			Guid sequentialguid;

			int hr = UuidCreateSequential(out sequentialguid);

			if (hr != RPC_S_OK)
			{
				throw new Win32Exception(hr, string.Format
("UuidCreateSequential() call failed: {0}", hr));
			}
			//ordered
			return SetSortOrder(sequentialguid);
		}

		private static Guid SetSortOrder(Guid guid)
		{
			byte[] bytes = guid.ToByteArray();
			var copyBytes = new byte[16];

			bytes.CopyTo(copyBytes, 0);

			for (int mapIndex = 0; mapIndex < 10; mapIndex++)
			{
				bytes[mapIndex] = copyBytes[_sqlOrderMap[mapIndex]];
			}

			guid = new Guid(bytes);
			return guid;
		} 
		#endregion
	}
}
